﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Gert3D
{
  /// <summary>
  /// Triangle represents a polygon with 3 vertices.
  /// </summary>
  public class Triangle
  {
    private Vertex _vertexA = null;
    private Vertex _vertexB = null;
    private Vertex _vertexC = null;
    private Box3 _bounds = Box3.Zero;
    private bool _boundsDone = false;

    /// <summary>
    /// Creates a new triangle.
    /// </summary>
    public Triangle()
    {
      _vertexA = new Vertex();
      _vertexB = new Vertex();
      _vertexC = new Vertex();
    }

    /// <summary>
    /// Creates a new triangle with the given vertices.
    /// </summary>
    /// <param name="va">Vertex A.</param>
    /// <param name="vb">Vertex B.</param>
    /// <param name="vc">Vertex C.</param>
    public Triangle(Vertex va, Vertex vb, Vertex vc)
    {
      _vertexA = va;
      _vertexB = vb;
      _vertexC = vc;
    }

    /// <summary>
    /// Creates a new triangle with the given vertex positions.
    /// </summary>
    /// <param name="xa">The x component of vertex A.</param>
    /// <param name="ya">The y component of vertex A.</param>
    /// <param name="za">The z component of vertex A.</param>
    /// <param name="xb">The x component of vertex B.</param>
    /// <param name="yb">The y component of vertex B.</param>
    /// <param name="zb">The z component of vertex B.</param>
    /// <param name="xc">The x component of vertex C.</param>
    /// <param name="yc">The y component of vertex C.</param>
    /// <param name="zc">The z component of vertex C.</param>
    public Triangle(double xa, double ya, double za, double xb, double yb, double zb, double xc, double yc, double zc)
    {
      _vertexA = new Vertex(xa, ya, za);
      _vertexB = new Vertex(xb, yb, zb);
      _vertexC = new Vertex(xc, yc, zc);
    }

    /// <summary>
    /// Gets or sets vertex A of the triangle.
    /// </summary>
    public Vertex VertexA
    {
      get { return _vertexA; }
      set { _vertexA = value; }
    }

    /// <summary>
    /// Gets or sets vertex B of the triangle.
    /// </summary>
    public Vertex VertexB
    {
      get { return _vertexB; }
      set { _vertexB = value; }
    }

    /// <summary>
    /// Gets or sets vertex C of the triangle.
    /// </summary>
    public Vertex VertexC
    {
      get { return _vertexC; }
      set { _vertexC = value; }
    }

    /// <summary>
    /// Calculates the normal of the triangle.
    /// </summary>
    /// <returns>The normalized normal of the triangle.</returns>
    public Vector3 GetNormal()
    {
      return Vector3.Normalize(Vector3.FromPoints(_vertexA.Position, _vertexC.Position).CrossProduct(Vector3.FromPoints(_vertexA.Position, _vertexB.Position)));
    }

    /// <summary>
    /// Calculates the inner angle at vertex A.
    /// </summary>
    /// <returns>The angle in radians.</returns>
    public double GetAngleAtA()
    {
      double a = _vertexB.Position.DistanceTo(_vertexC.Position);
      double b = _vertexA.Position.DistanceTo(_vertexC.Position);
      double c = _vertexA.Position.DistanceTo(_vertexB.Position);
      return Math.Acos((b * b + c * c - a * a) / (2 * b * c));
    }

    /// <summary>
    /// Calculates the inner angle at vertex B.
    /// </summary>
    /// <returns>The angle in radians.</returns>
    public double GetAngleAtB()
    {
      double a = _vertexB.Position.DistanceTo(_vertexC.Position);
      double b = _vertexA.Position.DistanceTo(_vertexC.Position);
      double c = _vertexA.Position.DistanceTo(_vertexB.Position);
      return Math.Acos((a * a + c * c - b * b) / (2 * a * c));
    }

    /// <summary>
    /// Calculates the inner angle at vertex C.
    /// </summary>
    /// <returns>The angle in radians.</returns>
    public double GetAngleAtC()
    {
      double a = _vertexB.Position.DistanceTo(_vertexC.Position);
      double b = _vertexA.Position.DistanceTo(_vertexC.Position);
      double c = _vertexA.Position.DistanceTo(_vertexB.Position);
      return Math.Acos((a * a + b * b - c * c) / (2 * a * b));
    }

    /// <summary>
    /// Calculates the perimeter of the triangle.
    /// </summary>
    /// <returns>The perimeter of the triangle.</returns>
    public double GetPerimeter()
    {
      double a = _vertexB.Position.DistanceTo(_vertexC.Position);
      double b = _vertexA.Position.DistanceTo(_vertexC.Position);
      double c = _vertexA.Position.DistanceTo(_vertexB.Position);
      return a + b + c;
    }

    /// <summary>
    /// Calculates the area of the triangle.
    /// </summary>
    /// <returns>The area of the triangle.</returns>
    public double GetArea()
    {
      Vector3 a = Vector3.FromPoints(_vertexA.Position, _vertexB.Position);
      Vector3 b = Vector3.FromPoints(_vertexB.Position, _vertexC.Position);
      Vector3 cross = Vector3.CrossProduct(a, b);
      return cross.Magnitude / 2;
    }

    /// <summary>
    /// Calculates the barycentric coordinates of the given point. This is done by
    /// projecting the triangle and the point to 2D space and then calculating
    /// the barycentric coordinates in 2D space. This only works if the point is
    /// on plane of the triangle!
    /// </summary>
    /// <param name="p">The point to calculate barycentric coordinates for.</param>
    /// <returns>The barycentric coordinates in a vector where X=C1, Y=C2 and Z=C3.</returns>
    public Vector3 GetBarycentricCoordinates(Vector3 p)
    {
      Vector3 normal = GetNormal();
      double max = Math.Max(Math.Abs(normal._x), Math.Max(Math.Abs(normal._y), Math.Abs(normal._z)));

      if (max == Math.Abs(normal._x))
        return GetBarycentricCoordinates(_vertexA.Position.Z, _vertexA.Position.Y, _vertexB.Position.Z, _vertexB.Position.Y, _vertexC.Position.Z, _vertexC.Position.Y, p.Z, p.Y);
      else if (max == Math.Abs(normal._y))
        return GetBarycentricCoordinates(_vertexA.Position.Z, _vertexA.Position.Y, _vertexB.Position.Z, _vertexB.Position.Y, _vertexC.Position.Z, _vertexC.Position.Y, p.Z, p.Y);
      else if (max == Math.Abs(normal._z))
        return GetBarycentricCoordinates(_vertexA.Position.X, _vertexA.Position.Y, _vertexB.Position.X, _vertexB.Position.Y, _vertexC.Position.X, _vertexC.Position.Y, p.X, p.Y);
      else
        return null;
    }

    /// <summary>
    /// Calculates the barycentric coordinates internally in 2D space.
    /// </summary>
    /// <param name="x1">The x component of the first vertex of the triangle.</param>
    /// <param name="y1">The y component of the first vertex of the triangle.</param>
    /// <param name="x2">The x component of the second vertex of the triangle.</param>
    /// <param name="y2">The y component of the second vertex of the triangle.</param>
    /// <param name="x3">The x component of the third vertex of the triangle.</param>
    /// <param name="y3">The y component of the third vertex of the triangle.</param>
    /// <param name="px">The x component of the point.</param>
    /// <param name="py">The y component of the point.</param>
    /// <returns>The barycentric coordinates in a vector where X=C1, Y=C2 and Z=C3.</returns>
    private Vector3 GetBarycentricCoordinates(double x1, double y1, double x2, double y2, double x3, double y3, double px, double py)
    {
      double A, B, C, D, E, F, AE, BD;

      A = x1 - x3;
      B = x2 - x3;
      C = x3 - px;

      D = y1 - y3;
      E = y2 - y3;
      F = y3 - py;

      AE = A * E;
      BD = B * D;

      Vector3 result = new Vector3();
      result._x = (B * F - C * E) / (AE - BD);
      result._y = (A * F - C * D) / (BD - AE);
      result._z = 1.0f - result._x - result._y;

      return result;
    }

    /// <summary>
    /// Interpolates the normal of the triangle at the given point by using the
    /// barycentric coordinates of the point. Note that the normal is also 
    /// calculated if the point is outside of the triangle.
    /// </summary>
    /// <param name="p">The point to interpolate the normal for.</param>
    /// <returns>The interpolated normalized normal vector.</returns>
    public Vector3 InterpolateNormal(double ba, double bb, double bc)
    {
      Vector3 result = new Vector3();
      result._x = (_vertexA.Normal._x * ba + _vertexB.Normal._x * bb + _vertexC.Normal._x * bc);
      result._y = (_vertexA.Normal._y * ba + _vertexB.Normal._y * bb + _vertexC.Normal._y * bc);
      result._z = (_vertexA.Normal._z * ba + _vertexB.Normal._z * bb + _vertexC.Normal._z * bc);
      result.Normalize();
      return result;
    }

    /// <summary>
    /// Interpolates the texture coordinates of the triangle at the given point 
    /// by using the barycentric coordinates of the point. Note that the texture 
    /// coordinates is also interpolated if the point is outside of the triangle.
    /// </summary>
    /// <param name="p">The point to interpolate the texture coordinates for.</param>
    /// <returns>The interpolated texture coordinates vector.</returns>
    public Vector3 InterpolateTexCoord(double ba, double bb, double bc)
    {
      Vector3 result = new Vector3();
      result._x = (_vertexA.TextureCoordinates.X * ba + _vertexB.TextureCoordinates.X * bb + _vertexC.TextureCoordinates.X * bc);
      result._y = (_vertexA.TextureCoordinates.Y * ba + _vertexB.TextureCoordinates.Y * bb + _vertexC.TextureCoordinates.Y * bc);
      result._z = (_vertexA.TextureCoordinates.Z * ba + _vertexB.TextureCoordinates.Z * bb + _vertexC.TextureCoordinates.Z * bc);
      return result;
    }

    /// <summary>
    /// Gets or sets the bounding box of the triangle that can be used for basic
    /// collision detection.
    /// </summary>
    public Box3 Bounds
    {
      get 
      {
        if (!_boundsDone)
        {
          _bounds = GetBounds();
          _boundsDone = true;
        }
        return _bounds; 
      }
      set { _bounds = value; }
    }

    private Box3 GetBounds()
    {
      return new Box3(
        Math.Min(_vertexA.Position.X, Math.Min(_vertexB.Position.X, _vertexC.Position.X)),
        Math.Max(_vertexA.Position.X, Math.Max(_vertexB.Position.X, _vertexC.Position.X)),
        Math.Max(_vertexA.Position.Y, Math.Max(_vertexB.Position.Y, _vertexC.Position.Y)),
        Math.Min(_vertexA.Position.Y, Math.Min(_vertexB.Position.Y, _vertexC.Position.Y)),
        Math.Max(_vertexA.Position.Z, Math.Max(_vertexB.Position.Z, _vertexC.Position.Z)),
        Math.Min(_vertexA.Position.Z, Math.Min(_vertexB.Position.Z, _vertexC.Position.Z)));
    }

  }
}
